package org.openapitools.codegen.languages;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Function;

import static org.openapitools.codegen.utils.StringUtils.*;

public abstract class AbstractRustCodegen extends DefaultCodegen implements CodegenConfig {

    private final Logger LOGGER = LoggerFactory.getLogger(AbstractRustCodegen.class);

    protected static final String VENDOR_EXTENSION_PARAM_IDENTIFIER = "x-rust-param-identifier";

    protected List<String> charactersToAllow = Collections.singletonList("_");
    protected Set<String> keywordsThatDoNotSupportRawIdentifiers = new HashSet<>(
            Arrays.asList("super", "self", "Self", "extern", "crate"));
    protected String enumSuffix = "";

    public AbstractRustCodegen() {
        super();
        // All 'Strict' and 'Reserved' keywords from https://doc.rust-lang.org/reference/keywords.html
        // Note: These are case-sensitive
        this.reservedWords = new HashSet<>(
                Arrays.asList(
                        "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
                        "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
                        "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
                        "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
                        "override", "priv", "typeof", "unsized", "virtual", "yield", "try"
                )
        );
    }

    @Override
    public GeneratorLanguage generatorLanguage() {
        return GeneratorLanguage.RUST;
    }

    @Override
    public String escapeQuotationMark(String input) {
        // remove " to avoid code injection
        return input.replace("\"", "");
    }

    @Override
    public String escapeUnsafeCharacters(String input) {
        return input.replace("*/", "*_/").replace("/*", "/_*");
    }

    @Override
    public boolean isReservedWord(String word) {
        // This is overridden to take account of Rust reserved words being case-sensitive.
        return word != null && reservedWords.contains(word);
    }

    /**
     * Determine the best fitting Rust type for an integer property. This is intended for use when a specific format
     * has not been defined in the specification. Where the minimum or maximum is not known then the returned type
     * will default to having at least 32 bits.
     *
     * @param minimum          The minimum value as set in the specification.
     * @param exclusiveMinimum If the minimum value itself is excluded by the specification.
     * @param maximum          The maximum value as set in the specification.
     * @param exclusiveMaximum If the maximum value itself is excluded by the specification.
     * @param preferUnsigned   Use unsigned types where the effective minimum is greater than or equal to zero.
     * @return The Rust data type name.
     */
    @VisibleForTesting
    public String bestFittingIntegerType(@Nullable BigInteger minimum,
                                         boolean exclusiveMinimum,
                                         @Nullable BigInteger maximum,
                                         boolean exclusiveMaximum,
                                         boolean preferUnsigned) {
        if (exclusiveMinimum) {
            minimum = Optional.ofNullable(minimum).map(min -> min.add(BigInteger.ONE)).orElse(null);
        }
        if (exclusiveMaximum) {
            maximum = Optional.ofNullable(maximum).map(max -> max.subtract(BigInteger.ONE)).orElse(null);
        }

        // If the minimum value is greater than or equal to zero, then it is safe to use an unsigned type
        boolean guaranteedPositive = Optional.ofNullable(minimum).map(min -> min.signum() >= 0).orElse(false);

        int requiredBits = Math.max(
                Optional.ofNullable(minimum).map(BigInteger::bitLength).orElse(0),
                Optional.ofNullable(maximum).map(BigInteger::bitLength).orElse(0)
        );

        // We will only enable the smaller types (less than 32 bits) if we know both the minimum and maximum
        boolean knownRange = !(Objects.isNull(minimum) || Objects.isNull(maximum));

        if (guaranteedPositive && preferUnsigned) {
            if (requiredBits <= 8 && knownRange) {
                return "u8";
            } else if (requiredBits <= 16 && knownRange) {
                return "u16";
            } else if (requiredBits <= 32) {
                return "u32";
            } else if (requiredBits <= 64) {
                return "u64";
            } else if (requiredBits <= 128) {
                return "u128";
            }
        } else {
            if (requiredBits <= 7 && knownRange) {
                return "i8";
            } else if (requiredBits <= 15 && knownRange) {
                return "i16";
            } else if (requiredBits <= 31) {
                return "i32";
            } else if (requiredBits <= 63) {
                return "i64";
            } else if (requiredBits <= 127) {
                return "i128";
            }
        }

        throw new RuntimeException("Number is too large to fit into i128");
    }

    /**
     * Determine if an integer property can be guaranteed to fit into an unsigned data type.
     *
     * @param minimum          The minimum value as set in the specification.
     * @param exclusiveMinimum If boundary values are excluded by the specification.
     * @return True if the effective minimum is greater than or equal to zero.
     */
    @VisibleForTesting
    public boolean canFitIntoUnsigned(@Nullable BigInteger minimum, boolean exclusiveMinimum) {
        return Optional.ofNullable(minimum).map(min -> {
            if (exclusiveMinimum) {
                min = min.add(BigInteger.ONE);
            }
            return min.signum() >= 0;
        }).orElse(false);
    }

    public enum CasingType {CAMEL_CASE, SNAKE_CASE}

    /**
     * General purpose sanitizing function for Rust identifiers (fields, variables, structs, parameters, etc.).<br>
     * Rules for Rust are fairly simple:
     * <ul>
     *     <li>Characters must belong to [A-Za-z0-9_]
     *     <li>Cannot use reserved words (but can sometimes prefix with "r#")
     *     <li>Cannot begin with a number
     * </ul>
     *
     * @param name                The input string
     * @param casingType          Which casing type to apply
     * @param escapePrefix        Prefix to escape words beginning with numbers or reserved words
     * @param type                The type of identifier (used for logging)
     * @param allowRawIdentifiers Raw identifiers can't always be used, because of filename vs import mismatch.
     * @return Sanitized string
     */
    public String sanitizeIdentifier(String name, CasingType casingType, String escapePrefix, String type, boolean allowRawIdentifiers) {
        String originalName = name;

        Function<String, String> casingFunction;
        switch (casingType) {
            case CAMEL_CASE:
                // This probably seems odd, but it is necessary for two reasons
                // Compatibility with rust-server, such that MyIDList => my_id_list => MyIdList
                // Conversion from SCREAMING_SNAKE_CASE to ScreamingSnakeCase
                casingFunction = (input) -> camelize(underscore(input));
                break;
            case SNAKE_CASE:
                casingFunction = StringUtils::underscore;
                break;
            default:
                throw new IllegalArgumentException("Unknown CasingType");
        }

        // Replace hyphens and periods with underscores
        name = name.replaceAll("[\\.\\-]", "_");

        // Apply special character escapes, e.g. "@type" => "At_type"
        // Remove the trailing underscore if necessary
        if (!Strings.isNullOrEmpty(name)) {
            boolean endedWithUnderscore = name.endsWith("_");
            name = escape(name, specialCharReplacements, charactersToAllow, "_");
            if (!endedWithUnderscore && name.endsWith("_")) {
                name = org.apache.commons.lang3.StringUtils.chop(name);
            }
        }

        // Sanitize any other special characters that weren't replaced
        name = sanitizeName(name);

        // Keep track of modifications prior to casing
        boolean nameWasModified = !originalName.equals(name);

        // Convert casing
        name = casingFunction.apply(name);

        // If word starts with number add a prefix
        // Note: this must be done after casing since CamelCase will strip leading underscores
        if (name.matches("^\\d.*")) {
            nameWasModified = true;
            name = casingFunction.apply(escapePrefix + '_' + name);
        }

        // Escape reserved words - this is case-sensitive so must be done after casing
        // There is currently a bug in Rust where this doesn't work for a few reserved words :(
        // https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094
        if (isReservedWord(name)) {
            nameWasModified = true;
            if (this.keywordsThatDoNotSupportRawIdentifiers.contains(name) || !allowRawIdentifiers) {
                name = casingFunction.apply(escapePrefix + '_' + name);
            } else {
                name = "r#" + name;
            }
        }

        // If the name had to be modified (not just because of casing), log the change
        if (nameWasModified) {
            LOGGER.warn("{} cannot be used as a {} name. Renamed to {}", casingFunction.apply(originalName), type, name);
        }

        return name;
    }

    @Override
    public String getTypeDeclaration(Schema p) {
        if (ModelUtils.isArraySchema(p)) {
            Schema inner = ModelUtils.getSchemaItems(p);
            String innerType = getTypeDeclaration(inner);
            return typeMapping.get("array") + "<" + innerType + ">";
        } else if (ModelUtils.isMapSchema(p)) {
            Schema inner = ModelUtils.getAdditionalProperties(p);
            String innerType = getTypeDeclaration(inner);
            StringBuilder typeDeclaration = new StringBuilder(typeMapping.get("map")).append("<").append(typeMapping.get("string")).append(", ");
            typeDeclaration.append(innerType).append(">");
            return typeDeclaration.toString();
        } else if (!org.apache.commons.lang3.StringUtils.isEmpty(p.get$ref())) {
            String datatype;
            try {
                datatype = "models::" + toModelName(ModelUtils.getSimpleRef(p.get$ref()));
            } catch (Exception e) {
                LOGGER.warn("Error obtaining the datatype from schema (model):{}. Datatype default to Object", p);
                datatype = "Object";
                LOGGER.error(e.getMessage(), e);
            }
            return datatype;
        } else if (p instanceof FileSchema) {
            return typeMapping.get("file");
        }

        String oasType = getSchemaType(p);
        if (typeMapping.containsKey(oasType)) {
            return typeMapping.get(oasType);
        }

        if (typeMapping.containsValue(oasType)) {
            return oasType;
        }

        if (languageSpecificPrimitives.contains(oasType)) {
            return oasType;
        }

        return "models::" + toModelName(oasType);
    }

    @Override
    public CodegenModel fromModel(String name, Schema model) {
        LOGGER.trace("Creating model from schema: {}", model);

        Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
        CodegenModel mdl = super.fromModel(name, model);

        mdl.vendorExtensions.put("x-upper-case-name", name.toUpperCase(Locale.ROOT));
        if (!org.apache.commons.lang3.StringUtils.isEmpty(model.get$ref())) {
            Schema schema = allDefinitions.get(ModelUtils.getSimpleRef(model.get$ref()));
            mdl.dataType = typeMapping.get(schema.getType());
        }
        if (ModelUtils.isArraySchema(model)) {
            if (typeMapping.containsKey(mdl.arrayModelType)) {
                mdl.arrayModelType = typeMapping.get(mdl.arrayModelType);
            } else {
                mdl.arrayModelType = toModelName(mdl.arrayModelType);
            }
        } else if ((!mdl.anyOf.isEmpty()) || (!mdl.oneOf.isEmpty())) {
            mdl.dataType = getSchemaType(model);
        }

        Schema additionalProperties = ModelUtils.getAdditionalProperties(model);

        if (additionalProperties != null) {
            mdl.additionalPropertiesType = getTypeDeclaration(additionalProperties);
        }

        LOGGER.trace("Created model: {}", mdl);

        return mdl;
    }

    @Override
    public String toVarName(String name) {
        // obtain the name from nameMapping directly if provided
        if (nameMapping.containsKey(name)) {
            return nameMapping.get(name);
        }

        return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "field/variable", true);
    }

    @Override
    public String toParamName(String name) {
        // obtain the name from parameterNameMapping directly if provided
        if (parameterNameMapping.containsKey(name)) {
            return parameterNameMapping.get(name);
        }

        return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "parameter", true);
    }

    @Override
    public String toOperationId(String operationId) {
        return sanitizeIdentifier(operationId, CasingType.SNAKE_CASE, "call", "method", true);
    }

    /// / Model naming ////

    protected String addModelNamePrefixAndSuffix(String name) {
        if (!Strings.isNullOrEmpty(modelNamePrefix)) {
            name = modelNamePrefix + "_" + name;
        }
        if (!Strings.isNullOrEmpty(modelNameSuffix)) {
            name = name + "_" + modelNameSuffix;
        }
        return name;
    }

    @Override
    public String toModelName(String name) {
        if (modelNameMapping.containsKey(name)) {
            return modelNameMapping.get(name);
        }

        return sanitizeIdentifier(addModelNamePrefixAndSuffix(name), CasingType.CAMEL_CASE, "model", "model", false);
    }

    @Override
    public String toModelFilename(String name) {
        return sanitizeIdentifier(addModelNamePrefixAndSuffix(name), CasingType.SNAKE_CASE, "model", "model file", false);
    }

    @Override
    public String toModelDocFilename(String name) {
        return toModelName(name);
    }

    /// / Enum naming ////
    @Override
    public String toEnumVarName(String name, String datatype) {
        if (enumNameMapping.containsKey(name)) {
            return enumNameMapping.get(name);
        }

        // Empty strings need to be mapped to "Empty"
        // https://github.com/OpenAPITools/openapi-generator/issues/13453
        if (Strings.isNullOrEmpty(name)) {
            return "Empty";
        }
        // Rust Enum variants should be camel cased
        return sanitizeIdentifier(name, CasingType.CAMEL_CASE, "variant", "enum variant", true);
    }

    @Override
    public String toEnumName(CodegenProperty property) {
        // Note: Strangely this function is only used for inline enums, schema enums go through the toModelName function
        String name = property.baseName;
        if (!Strings.isNullOrEmpty(enumSuffix)) {
            name = name + "_" + enumSuffix;
        }
        return sanitizeIdentifier(name, CasingType.CAMEL_CASE, "enum", "enum", false);
    }

    @Override
    public String toEnumValue(String value, String datatype) {
        // This is the representation of the enum that will be serialized / deserialized
        // Note: generators currently only support string enums, so checking the type here is pointless
        return escapeText(value);
    }

    @Override
    public String toEnumDefaultValue(String value, String datatype) {
        // TODO: Bug: currently the templates ignore this function and just use `Self::{{ enumVars.0.name }}`
        // Return the Rust type name of the variant so we can `impl Default` with it
        return toEnumVarName(value, datatype);
    }

    /// / API naming ////

    protected String addApiNamePrefixAndSuffix(String name) {
        if (Strings.isNullOrEmpty(name)) {
            name = "default";
        }
        if (!Strings.isNullOrEmpty(apiNamePrefix)) {
            name = apiNamePrefix + "_" + name;
        }
        if (!Strings.isNullOrEmpty(apiNameSuffix)) {
            name = name + "_" + apiNameSuffix;
        }
        return name;
    }

    @Override
    public String toApiName(String name) {
        return sanitizeIdentifier(addApiNamePrefixAndSuffix(name), CasingType.CAMEL_CASE, "api", "API", false);
    }

    @Override
    public String toApiFilename(String name) {
        return sanitizeIdentifier(addApiNamePrefixAndSuffix(name), CasingType.SNAKE_CASE, "api", "API file", false);
    }

    @Override
    public String toApiDocFilename(String name) {
        return toApiName(name);
    }

    @Override
    public String addRegularExpressionDelimiter(String pattern) {
        return pattern;
    }

    @Override
    public String escapeReservedWord(String name) {
        if (this.reservedWordsMappings().containsKey(name)) {
            return this.reservedWordsMappings().get(name);
        }
        return "r#" + name;
    }
}
